<?php declare(strict_types=1);

namespace App\Helper;

use App\Constants\ErrorCode;
use Hyperf\Support\Traits\StaticInstance;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Token;

class JwtHelper
{

    use StaticInstance;

    // token生成配置规则
    protected array $tokenConfig;

    protected Configuration $configuration;

    protected function __construct(array $params)
    {
        $this->tokenConfig = $params;
        $this->validateParams();
        $this->configuration = Configuration::forSymmetricSigner(
            new Sha256(),
            Key\InMemory::base64Encoded(base64_encode($this->tokenConfig['secret']))
        );
    }

    /**
     * 验证传入参数
     * @throws \Exception
     */
    protected function validateParams(): void
    {
        foreach (['jwt_id', 'secret', 'issuer', 'audience'] as $key) {
            if (!isset($this->tokenConfig[$key])) {
                throw new \Exception(__CLASS__ . '初始化缺少参数：' . $key);
            }
        }
    }

    /**
     * 生成token
     * @param array $rows
     * @param array $headers
     * @return string
     * @throws \Exception
     */
    public function encode(array $rows, array $headers = []): string
    {
        $config = $this->tokenConfig;
        $builder = $this->configuration->builder();
        if (!empty($config['issuer'])) $builder->issuedBy($config['issuer']);
        if (!empty($config['audience'])) $builder->permittedFor($config['audience']);
        if (!empty($config['jwt_id'])) $builder->identifiedBy($config['jwt_id']);

        if (is_numeric($config['duration']) && $config['duration'] > 0) {
            $issuedAt = date('Y-m-d H:i:s');
            $expiresAt = date('Y-m-d H:i:s', time() + $config['duration']);
            $builder->issuedAt(new \DateTimeImmutable($issuedAt, new \DateTimeZone('Asia/Shanghai')));
            $builder->expiresAt(new \DateTimeImmutable($expiresAt, new \DateTimeZone('Asia/Shanghai')));
        }

        //添加携带数据
        foreach ($headers as $column => $value) $builder->withHeader($column, $value);
        foreach ($rows as $column => $value) $builder->withClaim($column, $value);

        $key = Key\InMemory::plainText($this->tokenConfig['secret']);
        $token = $builder->getToken($this->configuration->signer(), $key);
        return $token->toString();
    }

    /**
     * 解密token
     * @param string $token
     * @return array
     * @throws \Exception
     */
    public function decode(string $token): array
    {
        $token = $this->configuration->parser()->parse($token);
        if ($this->validateToken($token)) {
            $rows = $token->claims()->all();
            $heads = $token->headers()->all();
            unset($rows['jti'], $rows['iat'], $rows['nbf'], $rows['exp'], $rows['sub'], $rows['aud'], $rows['iss']);
            if ($token->isExpired(new \DateTimeImmutable(date('Y-m-d H:i:s'), new \DateTimeZone('Asia/Shanghai')))) {
                return RespHelper::back(ErrorCode::ERR_GONE, '已过期');
            }
            return RespHelper::success(['rows' => $rows, 'heads' => $heads]);
        }
        return RespHelper::fail('验证失败');
    }

    /**
     * 验证token可用性
     * @param Token $token
     * @return bool
     */
    protected function validateToken(Token $token): bool
    {
        $claims = $token->claims();
        $configTkID = trim((string)$this->tokenConfig['jwt_id']);
        if (strlen($configTkID) > 0 && (string)$claims->get('jti') !== $configTkID) {
            return false;
        }
        $configTkIss = trim((string)$this->tokenConfig['issuer']);
        if (strlen($configTkIss) > 0 && (string)$claims->get('iss') !== $configTkIss) {
            return false;
        }
        $claimsAdu = $claims->get('aud');
        $configAdu = $this->tokenConfig['audience'];
        if(!empty($claimsAdu)){
            if (is_array($configAdu)) {
                if (!$configAdu == $claimsAdu) return false;
            } elseif (!in_array($configAdu, $claimsAdu)) {
                return false;
            }
        }
        return true;
    }
}